<?php
/*
    Release under the GNU public license
    Copyright Adam Stevenson adamstevenson@ _no_spam_  gmail.com (www.adamstevenson.net)
    If you use this script please send me an email, and let me know how it works for you,
    or any bugs you find. Also, if you want to link to my site I won't complain.

    Basically this is just a hash table over shared memory for caching php pages and 
    serialized objects.  You need to have php compiled with shared memory enabled.
    Simple garbage collection is implemented, but is only run when storing data in the
    cache and not returning pages to improve performance.

    It is very very fast.
    When caching was in place I was getting 55k byte pages returned over a local network
    in 0.02 ms, where before it was about 2.00 seconds.

    There is some support for debugging, but it is all commented out to gain a little
    more in performance.


    Note:  Versions of Windows previous to Windows 2000 do not support shared memory. 
    Under Windows, Shmop will only work when PHP is running as a web server module, 
    such as Apache or IIS (CLI and CGI will not work).

    Basic usage:

    For caching php rendered pages
        include 'mycached.php';
        do_cache();

    do_cache will automatically handle the garbage collection when storing pages.


    For caching php objects, you can use the store_value and get_value functions.

    store_value takes a key and value like any hashtable, and get_value just takes
    the key and returns the value if there is one.

    store_value('my_unique_key', $var);
    $var = get_value('my_unique_key');

    Directly accessing the store_value and get_value function does not do any
    garbage collection.  You can do this yourself by calling garbage_collection();
    in your scripts.


    To clear all the shared memory when testing, or what not you can use the unix
    command ipcs, and ipcrm.  Here is what I use which removes the shared memory
    segments, and also semaphores
    ipcs | cut -d" " -f2 | xargs -n1 ipcrm -s
    ipcs | cut -d" " -f2 | xargs -n1 ipcrm -m


    For even more robust caching, check out http://www.danga.com/memcached/

*/

DEFINE('CACHE_SECONDS', 60 * 60);
DEFINE('LOG', false);
DEFINE('LOG_FILE', '/tmp/debug.log' );
DEFINE('HASH_PRIME', 2147483647); //2^31 -1
DEFINE('LOCK', '/');
DEFINE('HASHID', 'hash_table_key');
 
function log_this($log_string){
    if(!LOG) return;
    $file = fopen(LOG_FILE, "a");
    fwrite($file, $log_string);
    fclose($file);
    return;
}

function delete_mem($id){
    if (!shmop_delete($id)){
        //log_this("Couldn't mark shared memory block for deletion.\n");
    }
}

function &read_mem($id){
    return shmop_read($id, 0, shmop_size($id));
}

function write_mem($id, $data){
    if(shmop_size($id)< strlen($data)) return false;
    if(!shmop_write($id, $data, 0)){
        //log_this("Could not write to shared memory segment\n");
        return false;
    }
    return true;
}

function create_mem($key, $size){
    $id = @shmop_open($key, "n", 0644, $size);
    if (!$id) {
        //log_this("Couldn't create shared memory segment with key = $key\n");
    }
    return $id;
}

function mem_exist($key){
    if(!$id = @shmop_open($key, "a", 0644, 100)){
        //log_this("Couldn't find shared memory segment with key = $key\n");
        return 0;
    }
    //log_this("Memory segment exists with key = $key\n");
    return $id;
}


function close_mem($id){
    if($id!=0) shmop_close($id);
}

function &get_value($key){
    $hash_id =& hash($key);
    $value = array();
    $id =& mem_exist($hash_id);
    while($value['key']!=$key){
        if($id!=0){
            $value =& unserialize(read_mem($id));
            if($value['key']!=$key) $id =& mem_exist(hash($hash_id));
        } else {
            //log_this("no key in hash table\n");
            close_mem($id);
            return "";
        }
    }
    close_mem($id);
    return $value['value'];
}

function store_value($key,$value){
    $SHM_KEY = ftok(LOCK, 'R');
    $shmid = @sem_get($SHM_KEY, 1024, 0644 | IPC_CREAT);
    sem_acquire($shmid);
    $hash_id = hash($key);
    $store_value = array();
    $store_value['key'] = trim($key);
    $store_value['value'] = $value;
    $store_value['time'] = time();
    $id = mem_exist($hash_id);
    while($id!=0){
        $value = unserialize(read_mem($id));
        if($value['key']==$key){
            //log_this("Key " . $key . "   $hash_id(" . dechex($hash_id) .") already in hash table, replacing with new contents\n");
            delete_mem($id);
            close_mem($id);
            $contents = serialize($store_value);
            $id = create_mem($hash_id,strlen($contents));
            write_mem($id, $contents); // place into memory
            close_mem($id);
            sem_release($shmid);
            return $hash_id;
        }
        close_mem($id);
        //log_this("Collision while trying to store key=$key in hash table\n");
        $hash_id = hash($hash_id);
        $id = mem_exist($hash_id);
    }
    $contents = serialize($store_value);
    $id = create_mem($hash_id,strlen($contents));
    write_mem($id, &$contents); // place into memory
    close_mem($id);
    sem_release($shmid);
    return $hash_id;
}

function update_keys($key, $id){
    $SHM_KEY = ftok(LOCK, 'R');
    $shmid = @sem_get($SHM_KEY, 1024, 0644 | IPC_CREAT);
    sem_acquire($shmid);
    $temp =& get_value(HASHID);
    $temp[$key] = array('shmid' => $id, 'time' => time());
    store_value(HASHID,$temp);
    sem_release($shmid);
}

function &hash($hash_string){
    $hash =& fmod(hexdec(md5($hash_string)), HASH_PRIME);
    //log_this("Hashing " . $hash_string . " to " . $hash . "\n"); 
    return $hash;
}

function cache_end($contents){
    if(trim($contents)){
        $datasize = strlen($contents);
        $hash_string = "http://".$_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"].serialize($_POST);
        $shmid = store_value($hash_string,$contents);
        update_keys($hash_string,$shmid);
    }
        return $contents; //display
}

function delete_key($key){
    $SHM_KEY = ftok(LOCK, 'R');
    $shmid = @sem_get($SHM_KEY, 1024, 0644 | IPC_CREAT);
    sem_acquire($shmid);
    $data = get_value(HASHID);
    if(isset($data[$key])){
        $v = $data[$key];
        $id = mem_exist($v['shmid']);
        delete_mem($id);
        close_mem($id);
        unset($data[$key]);
        store_value(HASHID, $data);
    }
    sem_release($shmid);
}

function clear_cache(){
    $SHM_KEY = ftok(LOCK, 'R');
    $shmid = @sem_get($SHM_KEY, 1024, 0644 | IPC_CREAT);
    sem_acquire($shmid);
    $data = get_value(HASHID);
    print_r($data);
    foreach ($data as $k => $v){
        $id = mem_exist($v['shmid']);
        delete_mem($id);
        close_mem($id);
    }
    $data = array();
    store_value(HASHID, $data);
    sem_release($shmid);
}


function garbage_collection(){
    $SHM_KEY = ftok(LOCK, 'R');
    $shmid = @sem_get($SHM_KEY, 1024, 0644 | IPC_CREAT);
    sem_acquire($shmid);
    $data = get_value(HASHID);
    foreach ($data as $k => $v){
        if(time() - $v['time'] > CACHE_SECONDS){
            //log_this("garbage collection found expired key $k, value $v[shmid] in hash table... deleting\n");
            $id = mem_exist($v['shmid']);
            delete_mem($id);
            close_mem($id);
            unset($data[$k]);
        }
    store_value(HASHID, $data);
    }
    sem_release($shmid);
}

function do_cache(){
    $key = "http://".$_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"].serialize($_POST);
    $contents =& get_value($key);
    if($contents){
        //log_this("Cache hit for " . $key . "\n");
        print $contents;
        exit;
    }
    garbage_collection();
    ob_start("cache_end"); // callback
}

?> 
